﻿// Copyright (c) JosephZheng. All rights reserved. See License.txt in the project root for license

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using Lyra.Cache.Server.Interface;
using Lyra.Cache.Server.Interface.Contract;
using Lyra.Cache.Server.Interface.Contract.ServerResult;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text;

namespace Lyra.Cache.Server
{
    public class CacheRegion : IDisposable
    {
        private DataTable _cache = null;
        private Thread _trdExpireCache = null;
        private Thread _trdReleaseLock = null;
        private int _lockSeconds = 30;
        private long _operation = 0;

        private Dictionary<string, DataView> _views = new Dictionary<string, DataView>();
        private Dictionary<string, Compiler> _extensions = new Dictionary<string, Compiler>();
        private Dictionary<string, object> _extensionobjects = new Dictionary<string, object>();

        public string CacheSchema { get; private set; }

        public CacheRegion(int cacheLockSeconds, string cacheSchema, Dictionary<string, string> cacheViews, Dictionary<string, string> extensions)
        {
            _lockSeconds = cacheLockSeconds;

            _cache = new DataTable();
            List<string> columnnames = new List<string>();
            _cache.PrimaryKey = new DataColumn[] { _cache.Columns.Add("_key", typeof(string)) };
            _cache.Columns.Add("_expire", typeof(DateTime));
            _cache.Columns.Add("_lock", typeof(DateTime));
            _cache.Columns.Add("_version", typeof(DateTime));
            _cache.Columns.Add("_session", typeof(string));
            CacheSchema = cacheSchema;
            foreach (string column in cacheSchema.Split(new char[] { '#' }, StringSplitOptions.RemoveEmptyEntries))
            {
                string[] columninfos = column.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                if (columninfos.Length == 2)
                {
                    try
                    {
                        _cache.Columns.Add(columninfos[0], Type.GetType(columninfos[1]));
                        columnnames.Add(columninfos[0].ToLower());
                    }
                    catch { }
                }
            }

            if (cacheViews != null && cacheViews.Count != 0)
            {
                foreach (KeyValuePair<string, string> view in cacheViews)
                {
                    List<string> checklist = new List<string>();
                    bool checkpass = true;
                    foreach (string columnname in view.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        string processedname = columnname.ToLower().Replace("asc", string.Empty).Replace("desc", string.Empty).Trim();
                        if (!columnnames.Contains(processedname))
                        {
                            checkpass = false;
                            break;
                        }
                        else
                        {
                            if (checklist.Contains(processedname))
                            {
                                checkpass = false;
                                break;
                            }
                            else checklist.Add(processedname);
                        }
                    }
                    if (checkpass)
                    {
                        try
                        {
                            _views.Add(view.Key, new DataView(_cache) { Sort = view.Value });
                        }
                        catch (Exception ex)
                        {
                        }
                    }
                }
            }

            if (extensions != null && extensions.Count != 0)
            {
                foreach (KeyValuePair<string, string> extension in extensions)
                {
                    try
                    {
                        Compiler cmp = new Compiler(extension.Key, extension.Value);
                        foreach (Type t in cmp.Assembly.GetTypes())
                        {
                            if (t.GetInterface("Lyra.Cache.Extension.IExtension", false) != null)
                            {
                                object obj = cmp.GetInstance(t.Name);
                                if(obj != null)
                                {
                                    _extensionobjects.Add(extension.Key, obj);
                                    this._extensions.Add(extension.Key, cmp);
                                    break;
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                    }
                }
            }

            _trdExpireCache = new Thread(new ThreadStart(ExpireCache));
            _trdExpireCache.IsBackground = true;
            _trdExpireCache.Start();

            _trdReleaseLock = new Thread(new ThreadStart(ReleaseLock));
            _trdReleaseLock.IsBackground = true;
            _trdReleaseLock.Start();
        }

        private void ExpireCache()
        {
            while (true)
            {
                try
                {
                    lock (_cache)
                    {
                        DataRow[] expires = _cache.Select(string.Format("_expire < '{0}'", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")));
                        foreach (DataRow row in expires) _cache.Rows.Remove(row);
                    }
                }
                catch { }
                finally
                {
                    Thread.Sleep(5000);
                }
            }
        }

        private void ReleaseLock()
        {
            while (true)
            {
                try
                {
                    lock (_cache)
                    {
                        DataRow[] releases = _cache.Select(string.Format("_lock < '{0}'", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")));
                        foreach (DataRow release in releases) release["_session"] = null;
                    }
                }
                catch { }
                finally
                {
                    Thread.Sleep(1000);
                }
            }
        }

        public void Dispose()
        {
            _trdReleaseLock.Abort();
            _trdExpireCache.Abort();
        }

        public bool Add(string key, string obj, TimeSpan expire)
        {
            Interlocked.Increment(ref _operation);
            Type t = obj.GetType();
            JObject jobj = JObject.Parse(obj.ToString());
            DataRow row = _cache.NewRow();
            row["_key"] = key;
            foreach (JProperty child in jobj.Children())
            {
                if (_cache.Columns.Contains(child.Name))
                {
                    JValue value = child.Value as JValue;
                    try
                    {
                        row[child.Name] = value.Value;
                    }
                    catch { }
                }
            }
            lock (_cache)
            {
                if (!_cache.Rows.Contains(key))
                {
                    _cache.Rows.Add(row);
                    DateTime now = DateTime.Now;
                    row["_expire"] = now.AddTicks(expire.Ticks);
                    row["_version"] = now;
                    return true;
                }
                else return false;
            }
        }

        public void Set(string key, string obj, TimeSpan expire, bool addIfNotExist)
        {
            this.SetWithLock(key, obj, expire, null, addIfNotExist);
        }

        public void SetWithLock(string key, string obj, TimeSpan expire, string lockSession, bool addIfNotExist)
        {
            Interlocked.Increment(ref _operation);
            lock (_cache)
            {
                JObject jobj = JObject.Parse(obj.ToString());
                if (!_cache.Rows.Contains(key))
                {
                    if (addIfNotExist)
                    {
                        DataRow row = _cache.NewRow();
                        row["_key"] = key;
                        foreach (JProperty child in jobj.Children())
                        {
                            if (_cache.Columns.Contains(child.Name))
                            {
                                JValue value = child.Value as JValue;
                                try
                                {
                                    row[child.Name] = value.Value;
                                }
                                catch { }
                            }
                        }
                        DateTime now = DateTime.Now;
                        row["_expire"] = now.AddTicks(expire.Ticks);
                        row["_version"] = now;
                        _cache.Rows.Add(row);
                    }
                }
                else
                {
                    DataRow row = _cache.Select(string.Format("_key = '{0}'", key))[0];
                    string session = row["_session"] as string;
                    if (string.IsNullOrWhiteSpace(session) || session == lockSession)
                    {
                        foreach (JProperty child in jobj.Children())
                        {
                            if (_cache.Columns.Contains(child.Name))
                            {
                                JValue value = child.Value as JValue;
                                try
                                {
                                    row[child.Name] = value.Value;
                                }
                                catch { }
                            }
                        }
                        DateTime now = DateTime.Now;
                        row["_expire"] = now.AddTicks(expire.Ticks);
                        row["_version"] = now;
                    }
                }
            }
        }

        internal GetServerResult ConvertItem(DataRow row, string lockSession, int lockSeconds)
        {
            if (string.IsNullOrWhiteSpace(row["_session"] as string))
            {
                JObject obj = new JObject();
                foreach (DataColumn column in _cache.Columns)
                {
                    if (column.ColumnName != "_key")
                    {
                        obj.Add(column.ColumnName, new JValue(row[column]));
                    }
                }
                if (!string.IsNullOrWhiteSpace(lockSession))
                {
                    row["_session"] = lockSession;
                    if (lockSeconds <= 0)
                    {
                        row["_lock"] = DateTime.Now.AddSeconds(_lockSeconds);
                    }
                    else
                    {
                        row["_lock"] = DateTime.Now.AddSeconds(lockSeconds);
                    }
                }
                return new GetServerResult() { Json = new CacheJson() { Data = JsonConvert.SerializeObject(obj), Time = (DateTime)row["_version"] } };
            }
            else return new GetServerResult() { Code = CacheCode.HadBeenLocked };
        }

        public GetServerResult Get(string key)
        {
            return this.GetWithLock(key, null,0);
        }

        public GetServerResult GetWithLock(string key, string lockSession, int lockSeconds)
        {
            Interlocked.Increment(ref _operation);
            lock (_cache)
            {
                DataRow[] rows = _cache.Select(string.Format("_key = '{0}'", key));
                if (rows != null && rows.Length != 0)
                {
                    return ConvertItem(rows[0], lockSession, lockSeconds);
                }
                else return new GetServerResult() { Code = CacheCode.KeyNotExist };
            }
        }

        public Dictionary<string, GetServerResult> GetWithFilter(string filterExpression)
        {
            return this.GetWithFilterAndLock(filterExpression, null,0);
        }

        public Dictionary<string, GetServerResult> GetWithFilterUsingView(string filterExpression, string view)
        {
            return this.GetWithFilterAndLockUsingView(filterExpression, view, null,0);
        }

        public Dictionary<string, GetServerResult> GetWithFilterAndLock(string filterExpression, string lockSession, int lockSeconds)
        {
            Interlocked.Increment(ref _operation);
            Dictionary<string, GetServerResult> result = new Dictionary<string, GetServerResult>();
            lock (_cache)
            {
                foreach (DataRow row in _cache.Select(filterExpression))
                {
                    result.Add(row["_key"].ToString(), this.ConvertItem(row, lockSession,lockSeconds));
                }
            }
            return result;
        }

        public Dictionary<string, GetServerResult> GetWithFilterAndLockUsingView(string filterExpression, string view, string lockSession, int lockSeconds)
        {
            Interlocked.Increment(ref _operation);
            Dictionary<string, GetServerResult> result = new Dictionary<string, GetServerResult>();
            if (_views.ContainsKey(view))
            {
                lock (_cache)
                {
                    _views[view].RowFilter = filterExpression;
                    foreach (DataRowView row in _views[view])
                    {
                        result.Add(row["_key"].ToString(), this.ConvertItem(row.Row, lockSession,lockSeconds));
                    }
                }
            }
            return result;
        }

        public Dictionary<string, GetServerResult> GetWithFilterAndTop(string filterExpression, int top, string sort)
        {
            return this.GetWithFilterAndTopAndLock(filterExpression, top, sort, null,0);
        }

        public Dictionary<string, GetServerResult> GetWithFilterAndTopUsingView(string filterExpression, string view, int top, string sort)
        {
            return this.GetWithFilterAndTopAndLockUsingView(filterExpression, view, top, sort, null,0);
        }

        public Dictionary<string, GetServerResult> GetWithFilterAndTopAndLock(string filterExpression, int top, string sort, string lockSession, int lockSeconds)
        {
            Interlocked.Increment(ref _operation);
            Dictionary<string, GetServerResult> result = new Dictionary<string, GetServerResult>();
            lock (_cache)
            {
                foreach (DataRow row in _cache.Select(filterExpression, sort).Take(top))
                {
                    result.Add(row["_key"].ToString(), this.ConvertItem(row, lockSession,lockSeconds));
                }
            }
            return result;
        }

        public Dictionary<string, GetServerResult> GetWithFilterAndTopAndLockUsingView(string filterExpression, string view, int top, string sort, string lockSession, int lockSeconds)
        {
            Interlocked.Increment(ref _operation);
            Dictionary<string, GetServerResult> result = new Dictionary<string, GetServerResult>();
            if (_views.ContainsKey(view))
            {
                lock (_cache)
                {
                    _views[view].RowFilter = filterExpression;
                    foreach (DataRow row in _views[view].ToTable().Select("1 = 1",sort).Take(top))
                    {
                        result.Add(row["_key"].ToString(), this.ConvertItem(_cache.Rows.Find(row["_key"].ToString()), lockSession,lockSeconds));
                    }
                }
            }
            return result;
        }

        public Dictionary<string, GetServerResult> GetWithExtension(string extensionName, string extensionValue)
        {
            return this.GetWithExtensionAndLock(extensionName, extensionValue,null,0);
        }

        public Dictionary<string, GetServerResult> GetWithExtensionAndLock(string extensionName, string extensionValue, string lockSession, int lockSeconds)
        {
            Interlocked.Increment(ref _operation);
            Dictionary<string, GetServerResult> result = new Dictionary<string, GetServerResult>();
            if (_extensions.ContainsKey(extensionName))
            {
                lock (_cache)
                {
                    foreach (DataRow row in _cache.Rows)
                    {
                        if (_extensions[extensionName].InvokeMethod<bool>(_extensionobjects[extensionName], "Filter", row, extensionValue))
                        {
                            result.Add(row["_key"].ToString(), this.ConvertItem(row, lockSession, lockSeconds));
                        }
                    }
                }
            }
            return result;
        }

        public long Count()
        {
            Interlocked.Increment(ref _operation);
            lock (_cache)
            {
                return _cache.Rows.Count;
            }
        }

        public List<string> Count2()
        {
            Interlocked.Increment(ref _operation);
            lock (_cache)
            {
                return (from row in _cache.AsEnumerable() select row["_key"] as string).ToList();
            }
        }

        public List<string> Count2WithFilter(string filterExpression)
        {
            Interlocked.Increment(ref _operation);
            lock (_cache)
            {
                return _cache.Select(filterExpression).Select(row => row["_key"].ToString()).ToList();
            }
        }

        public List<string> Count2WithExtension(string extensionName, string extensionValue)
        {
            Interlocked.Increment(ref _operation);
            List<string> result = new List<string>();
            if (_extensions.ContainsKey(extensionName))
            {
                lock (_cache)
                {
                    foreach (DataRow row in _cache.Rows)
                    {
                        if (_extensions[extensionName].InvokeMethod<bool>(_extensionobjects[extensionName], "Filter", row, extensionValue))
                        {
                            result.Add(row["_key"].ToString());
                        }
                    }
                }
            }
            return result;
        }

        public void Remove(string key)
        {
            this.RemoveWithLock(key, null);
        }

        public void RemoveWithLock(string key, string lockSession)
        {
            Interlocked.Increment(ref _operation);
            lock (_cache)
            {
                DataRow[] rows = _cache.Select(string.Format("_key = '{0}'", key));
                if (rows != null && rows.Length != 0)
                {
                    if (string.IsNullOrWhiteSpace(rows[0]["_session"] as string))
                    {
                        _cache.Rows.Remove(rows[0]);
                    }
                    else
                    {
                        if (lockSession == rows[0]["_session"].ToString())
                        {
                            _cache.Rows.Remove(rows[0]);
                        }
                    }
                }
            }
        }

        public CacheStatistics Statistics()
        {
            CacheStatistics result = new CacheStatistics()
            {
                Operation = Interlocked.Increment(ref _operation),
                Schema = this.CacheSchema,
                Count = this._cache.Rows.Count,
            };
            if (_views.Count != 0)
            {
                StringBuilder sb = new StringBuilder();
                foreach (KeyValuePair<string, DataView> kvp in _views)
                {
                    sb.Append(kvp.Key);
                    sb.Append(";");
                    sb.Append(kvp.Value.Sort);
                    sb.Append("#");
                }
                result.Views = sb.Remove(sb.Length - 1, 1).ToString();
            }
            if (_extensions.Count != 0)
            {
                result.Extensions = string.Join("#", _extensions.Keys.ToArray());
            }
            return result;
        }

        internal void Release(double percentage)
        {
            lock (_cache)
            {
                int count = (int)(_cache.Rows.Count * percentage);
                if (_cache.Rows.Count > count)
                {
                    DataRow[] rows = _cache.Select("1 = 1", "_expire asc").Take(count).ToArray();
                    foreach (DataRow row in rows) _cache.Rows.Remove(row);
                }
            }
        }
    }
}
